package org.infodavid.common.reflect;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The Class MethodUtil.
 */
@SuppressWarnings(value = {
    "rawtypes", "unchecked"
})
public final class MethodUtil {

	/** The Constant DEFAULT_VALUES. */
	public static final Map<Class,Object> DEFAULT_VALUES;

	/** The Constant log. */
	private static final Log LOGGER = LogFactory.getLog(MethodUtil.class);// NOPMD

	/** The singleton. */
	private static WeakReference<MethodUtil> singleton = null;

	static {
		DEFAULT_VALUES = new HashMap<Class,Object>();
	}

	/**
	 * returns the singleton.
	 * @return the singleton
	 */
	public static MethodUtil getInstance() {
		if (singleton == null || singleton.get() == null) {
			singleton = new WeakReference<MethodUtil>(new MethodUtil());
		}

		return singleton.get();
	}

	/**
	 * Instantiates a new reflect util.
	 */
	private MethodUtil() {
		super();
	}

	/**
	 * Checks if is modifier.
	 * @param modifier the modifier
	 * @param expected the expected
	 * @return true, if is modifier
	 */
	public boolean isModifier(final int modifier, final int expected) {
		return (modifier & (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED)) == expected;
	}

	/**
	 * Gets the method.
	 * @param clazz the class
	 * @param name the method name
	 * @return the method
	 */
	public Method findMethod(final Class clazz, final String name) {
		Class current = clazz;
		Method result = null;
		Method[] methods;
		Method method;
		int i;

		while (result == null && current != null) {
			try {
				methods = current.getDeclaredMethods();
				i = 0;

				if (methods != null) {
					while (result == null && i < methods.length) {
						method = methods[i++];

						if (name.equals(method.getName())) {
							result = method;
						}
					}
				}
			}
			catch (final SecurityException e) {}
			// not found, search in the super class
			current = current.getSuperclass();
		}

		return result;
	}

	/**
	 * Gets the getter method according to the name of the field.
	 * @param clazz the class where to read data
	 * @param field the field
	 * @return the getter
	 */
	public Method getGetter(final Class clazz, final Field field) {
		String prefix;

		if (field.getType() == boolean.class || field.getType() == Boolean.class) {
			prefix = "is";
		}
		else {
			prefix = "get";
		}

		Class current = clazz;
		Method method = null;

		while (current != null && method == null) {
			try {
				// example: getUuid or isUuid
				method = current.getDeclaredMethod(prefix + StringUtils.capitalize(field.getName()));
			}
			catch (final NoSuchMethodException e) {
				try {
					// example: getUUID or isUUID
					method = current.getDeclaredMethod(prefix + field.getName().toUpperCase());
				}
				catch (final NoSuchMethodException e2) {
					try {
						// example: getUUIDs or isUUIDs
						String name = field.getName().toUpperCase();

						name =
						    name.substring(0, name.length() - 1)
						        + Character.toLowerCase(name.charAt(name.length() - 1));

						method = current.getDeclaredMethod(prefix + name);
					}
					catch (final NoSuchMethodException e3) {}
				}
			}

			// not found, search in the super class
			current = current.getSuperclass();
		}

		return method;
	}

	/**
	 * Gets the getters.
	 * @param clazz the class
	 * @return the getters
	 */
	public Map<String,Method> getGetters(final Class clazz) {
		final IMethodFilter filter = new IMethodFilter() {
			public boolean accept(final Method method) {
				return method.getReturnType() != null
				    && (method.getParameterTypes() == null || method.getParameterTypes().length == 0)
				    && method.getName().matches("((is)|(get)).*");
			}
		};

		return getMethods(clazz, filter);
	}

	/**
	 * Gets the methods.
	 * @param clazz the class
	 * @param filter the filter
	 * @return the methods
	 */
	public Map<String,Method> getMethods(final Class clazz, final IMethodFilter filter) {
		Class current = clazz;
		final Map<String,Method> results = new HashMap<String,Method>();
		Method[] methods;
		Method method;
		int i;

		while (current != null) {
			try {
				methods = current.getDeclaredMethods();
				i = 0;

				if (methods != null) {
					while (i < methods.length) {
						method = methods[i++];

						if (filter == null || filter.accept(method)) {
							if (!results.containsKey(method.getName())) {
								results.put(method.getName(), method);
							}
						}
					}
				}
			}
			catch (final SecurityException e) {}
			// not found, search in the super class
			current = current.getSuperclass();
		}

		return results;
	}

	/**
	 * Gets the setter method according to the name of the field.
	 * @param clazz the class where to write data
	 * @param field the field
	 * @return the setter
	 */
	public Method getSetter(final Class clazz, final Field field) {
		Class current = clazz;
		Method method = null;
		final String prefix = "set";

		while (current != null && method == null) {
			try {
				// example: setUuid
				method =
				    current.getDeclaredMethod(prefix + StringUtils.capitalize(field.getName()),
				        field.getType());
			}
			catch (final NoSuchMethodException e) {
				try {
					// example: setUUID
					method =
					    current.getDeclaredMethod(prefix + field.getName().toUpperCase(), field.getType());
				}
				catch (final NoSuchMethodException e2) {
					try {
						// example: setUUIDs
						String name = field.getName().toUpperCase();

						name =
						    name.substring(0, name.length() - 1)
						        + Character.toLowerCase(name.charAt(name.length() - 1));

						method = current.getDeclaredMethod(prefix + name, field.getType());
					}
					catch (final NoSuchMethodException e3) {}
				}
			}

			// not found, search in the super class
			if (current.getSuperclass() == current) {
				current = null;
			}
			else {
				current = current.getSuperclass();
			}
		}

		return method;
	}

	/**
	 * Gets the setters.
	 * @param clazz the class
	 * @return the setters
	 */
	public Map<String,Method> getSetters(final Class clazz) {
		final IMethodFilter filter = new IMethodFilter() {
			public boolean accept(final Method method) {
				return (method.getReturnType() == null || method.getReturnType() == Void.TYPE)
				    && method.getParameterTypes() != null && method.getParameterTypes().length == 1
				    && method.getName().startsWith("set");
			}
		};

		return getMethods(clazz, filter);
	}

	/**
	 * Invoke.
	 * @param method the method
	 * @param instance the instance
	 * @param parameters the parameters
	 * @return the object
	 * @throws Exception the exception
	 */
	public Object invoke(final Method method, final Object instance, final Object... parameters)
	    throws Exception {
		Object result;

		method.setAccessible(true);

		if (parameters == null || parameters.length == 0) {
			result = method.invoke(instance);
		}
		else {
			result = method.invoke(instance, parameters);
		}

		return result;
	}

	/**
	 * Gets the setter name.
	 * @param getter the getter
	 * @return the setter name
	 */
	public String getSetterName(final Method getter) {
		String result;

		if (getter == null) {
			result = null;
		}
		else {
			final String name = getter.getName();

			if (name.startsWith("get")) {
				result = name.substring(3);
			}
			else if (name.startsWith("is")) {
				result = name.substring(2);
			}
			else {
				result = name;
			}
		}

		if (result != null) {
			result = "set" + result;
		}

		return result;
	}

	/**
	 * Gets the getter name.
	 * @param setter the setter
	 * @return the getter name
	 */
	public String getGetterName(final Method setter) {
		String result;

		if (setter == null) {
			result = null;
		}
		else {
			final String name = setter.getName();

			if (name.startsWith("set")) {
				result = name.substring(3);
			}
			else {
				result = name;
			}
		}

		if (!(result == null || setter == null)) {
			final Class[] paramTypes = setter.getParameterTypes();

			if (paramTypes[0] == boolean.class || paramTypes[0] == Boolean.class) {
				result = "is" + result;
			}
			else {
				result = "get" + result;
			}
		}

		return result;
	}

	/**
	 * Merge.
	 * @param partial the partial
	 * @param target the target
	 * @param gettersToIgnore the getters to ignore
	 * @throws IllegalArgumentException the illegal argument exception
	 * @throws IllegalAccessException the illegal access exception
	 * @throws InvocationTargetException the invocation target exception
	 */
	public void merge(final Object partial, final Object target, final Set<String> gettersToIgnore)
	    throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		final Map<String,Method> setters = getSetters(target.getClass());
		final Map<String,Method> getters = getGetters(target.getClass());
		final boolean isDebugEnabled = LOGGER.isDebugEnabled();
		Method getter;
		Method setter;
		Object value;

		for (final Map.Entry<String,Method> entry : getters.entrySet()) {
			if (gettersToIgnore != null && gettersToIgnore.contains(entry.getKey())) {
				if (isDebugEnabled) {
					LOGGER.debug("Skipping, no value for method: " + entry.getKey());
				}
			}
			// no need to merge data from core java classes
			else if (entry.getValue().getDeclaringClass() != Object.class) {
				getter = entry.getValue();

				try {
					value = getter.invoke(partial);
				}
				catch (final Exception e) {
					LOGGER.warn("No such getter method " + entry.getKey() + " in source object: "
					    + partial.getClass());

					value = null;
				}

				if (value == null) {
					if (isDebugEnabled) {
						LOGGER.debug("Skipping, no value for method: " + entry.getKey());
					}
				}
				else if (value instanceof Collection) {
					final Collection from = (Collection)value;
					final Collection to = (Collection)getter.invoke(target);

					// no duplicated
					for (final Object item : from) {
						if (to.contains(item)) {
							to.remove(item);
						}
					}

					to.addAll(from);
				}
				else if (value instanceof Map) {
					final Map to = (Map)getter.invoke(target);

					to.putAll((Map)value);
				}
				else {
					setter = setters.get(getSetterName(getter));

					if (setter != null && isModifier(setter.getModifiers(), Modifier.PUBLIC)) {
						if (isDebugEnabled) {
							LOGGER.debug("Using setter: " + entry.getKey());
						}

						try {
							setter.invoke(target, value);
						}
						catch (final Exception e) {
							LOGGER.warn("An error occurs on setter method " + setter.getName()
							    + " in target object: " + target.getClass(), e);
						}
					}
					else {
						if (isDebugEnabled) {
							LOGGER.debug("No public setter: " + entry.getKey());
						}

						merge(getter.invoke(partial), getter.invoke(target), gettersToIgnore);
					}
				}
			}
		}
	}
}
